/* eslint-disable */
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import classNames from 'classnames'
import warning from 'warning'
import {hasProp, initDefaultProps, getOptionProps, getSlots} from 'ant-design-vue/es/_util/props-util'
import {cloneElement} from 'ant-design-vue/es/_util/vnode'
import BaseMixin from 'ant-design-vue/es/_util/BaseMixin'
import proxyComponent from 'ant-design-vue/es/_util/proxyComponent'
import {
    convertTreeToEntities,
    convertDataToTree,
    getPosition,
    getDragNodesKeys,
    parseCheckedKeys,
    conductExpandParent,
    calcSelectedKeys,
    calcDropPosition,
    arrAdd,
    arrDel,
    posToArr,
    mapChildren,
    conductCheck,
    warnOnlyTreeNode
} from './util'

/**
 * Thought we still use `cloneElement` to pass `key`,
 * other props can pass with context for future refactor.
 */

function getWatch(keys = []) {
    const watch = {}
    keys.forEach(k => {
        watch[k] = function () {
            this.needSyncKeys[k] = true
        }
    })
    return watch
}

const Tree = {
    name: 'Tree',
    mixins: [BaseMixin],
    props: initDefaultProps(
        {
            prefixCls: PropTypes.string,
            tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
            children: PropTypes.any,
            treeData: PropTypes.array, // Generate treeNode by children
            showLine: PropTypes.bool,
            showIcon: PropTypes.bool,
            icon: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
            focusable: PropTypes.bool,
            selectable: PropTypes.bool,
            disabled: PropTypes.bool,
            multiple: PropTypes.bool,
            checkable: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
            checkStrictly: PropTypes.bool,
            draggable: PropTypes.bool,
            defaultExpandParent: PropTypes.bool,
            autoExpandParent: PropTypes.bool,
            defaultExpandAll: PropTypes.bool,
            defaultExpandedKeys: PropTypes.array,
            expandedKeys: PropTypes.array,
            defaultCheckedKeys: PropTypes.array,
            checkedKeys: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
            defaultSelectedKeys: PropTypes.array,
            selectedKeys: PropTypes.array,
            // onClick: PropTypes.func,
            // onDoubleClick: PropTypes.func,
            // onExpand: PropTypes.func,
            // onCheck: PropTypes.func,
            // onSelect: PropTypes.func,
            loadData: PropTypes.func,
            loadedKeys: PropTypes.array,
            // onMouseEnter: PropTypes.func,
            // onMouseLeave: PropTypes.func,
            // onRightClick: PropTypes.func,
            // onDragStart: PropTypes.func,
            // onDragEnter: PropTypes.func,
            // onDragOver: PropTypes.func,
            // onDragLeave: PropTypes.func,
            // onDragEnd: PropTypes.func,
            // onDrop: PropTypes.func,
            filterTreeNode: PropTypes.func,
            openTransitionName: PropTypes.string,
            openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
            switcherIcon: PropTypes.any,
            _propsSymbol: PropTypes.any
        },
        {
            prefixCls: 'rc-tree',
            showLine: false,
            showIcon: true,
            selectable: true,
            multiple: false,
            checkable: false,
            disabled: false,
            checkStrictly: false,
            draggable: false,
            defaultExpandParent: true,
            autoExpandParent: false,
            defaultExpandAll: false,
            defaultExpandedKeys: [],
            defaultCheckedKeys: [],
            defaultSelectedKeys: []
        }
    ),

    data() {
        warning(this.$props.__propsSymbol__, 'must pass __propsSymbol__')
        warning(this.$props.children, 'please use children prop replace slots.default')
        this.needSyncKeys = {}
        this.domTreeNodes = {}
        const state = {
            _posEntities: new Map(),
            _keyEntities: new Map(),
            _expandedKeys: [],
            _selectedKeys: [],
            _checkedKeys: [],
            _halfCheckedKeys: [],
            _loadedKeys: [],
            _loadingKeys: [],
            _treeNode: [],
            _prevProps: null,
            _dragOverNodeKey: '',
            _dropPosition: null,
            _dragNodesKeys: []
        }
        return {
            ...state,
            ...this.getDerivedState(getOptionProps(this), state)
        }
    },
    provide() {
        return {
            vcTree: this
        }
    },

    watch: {
        ...getWatch([
            'treeData',
            'children',
            'expandedKeys',
            'autoExpandParent',
            'selectedKeys',
            'checkedKeys',
            'loadedKeys'
        ]),
        __propsSymbol__() {
            this.setState(this.getDerivedState(getOptionProps(this), this.$data))
            this.needSyncKeys = {}
        }
    },

    methods: {
        getDerivedState(props, prevState) {
            const {_prevProps} = prevState
            const newState = {
                _prevProps: {...props}
            }
            const self = this

            function needSync(name) {
                return (!_prevProps && name in props) || (_prevProps && self.needSyncKeys[name])
            }

            // ================== Tree Node ==================
            let treeNode = null

            // Check if `treeData` or `children` changed and save into the state.
            if (needSync('treeData')) {
                treeNode = convertDataToTree(this.$createElement, props.treeData)
            } else if (needSync('children')) {
                treeNode = props.children
            }

            // Tree support filter function which will break the tree structure in the vdm.
            // We cache the treeNodes in state so that we can return the treeNode in event trigger.
            if (treeNode) {
                newState._treeNode = treeNode

                // Calculate the entities data for quick match
                const entitiesMap = convertTreeToEntities(treeNode)
                newState._keyEntities = entitiesMap.keyEntities
            }

            const keyEntities = newState._keyEntities || prevState._keyEntities

            // ================ expandedKeys =================
            if (needSync('expandedKeys') || (_prevProps && needSync('autoExpandParent'))) {
                newState._expandedKeys =
                    props.autoExpandParent || (!_prevProps && props.defaultExpandParent)
                        ? conductExpandParent(props.expandedKeys, keyEntities)
                        : props.expandedKeys
            } else if (!_prevProps && props.defaultExpandAll) {
                newState._expandedKeys = [...keyEntities.keys()]
            } else if (!_prevProps && props.defaultExpandedKeys) {
                newState._expandedKeys =
                    props.autoExpandParent || props.defaultExpandParent
                        ? conductExpandParent(props.defaultExpandedKeys, keyEntities)
                        : props.defaultExpandedKeys
            }

            // ================ selectedKeys =================
            if (props.selectable) {
                if (needSync('selectedKeys')) {
                    newState._selectedKeys = calcSelectedKeys(props.selectedKeys, props)
                } else if (!_prevProps && props.defaultSelectedKeys) {
                    newState._selectedKeys = calcSelectedKeys(props.defaultSelectedKeys, props)
                }
            }

            // ================= checkedKeys =================
            if (props.checkable) {
                let checkedKeyEntity

                if (needSync('checkedKeys')) {
                    checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {}
                } else if (!_prevProps && props.defaultCheckedKeys) {
                    checkedKeyEntity = parseCheckedKeys(props.defaultCheckedKeys) || {}
                } else if (treeNode) {
                    // If treeNode changed, we also need check it
                    checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {
                        checkedKeys: prevState._checkedKeys,
                        halfCheckedKeys: prevState._halfCheckedKeys
                    }
                }

                if (checkedKeyEntity) {
                    let {checkedKeys = [], halfCheckedKeys = []} = checkedKeyEntity

                    if (!props.checkStrictly) {
                        const conductKeys = conductCheck(checkedKeys, true, keyEntities);
                        ({checkedKeys, halfCheckedKeys} = conductKeys)
                    }

                    newState._checkedKeys = checkedKeys
                    newState._halfCheckedKeys = halfCheckedKeys
                }
            }
            // ================= loadedKeys ==================
            if (needSync('loadedKeys')) {
                newState._loadedKeys = props.loadedKeys
            }

            return newState
        },
        onNodeDragStart(event, node) {
            const {_expandedKeys} = this.$data
            const {eventKey} = node
            const children = getSlots(node).default
            this.dragNode = node

            this.setState({
                _dragNodesKeys: getDragNodesKeys(
                    typeof children === 'function' ? children() : children,
                    node
                ),
                _expandedKeys: arrDel(_expandedKeys, eventKey)
            })
            this.__emit('dragstart', {event, node})
        },

        /**
         * [Legacy] Select handler is less small than node,
         * so that this will trigger when drag enter node or select handler.
         * This is a little tricky if customize css without padding.
         * Better for use mouse move event to refresh drag state.
         * But let's just keep it to avoid event trigger logic change.
         */
        onNodeDragEnter(event, node) {
            const {_expandedKeys: expandedKeys} = this.$data
            const {pos, eventKey} = node

            if (!this.dragNode || !node.$refs.selectHandle) return

            const dropPosition = calcDropPosition(event, node)

            // Skip if drag node is self
            if (this.dragNode.eventKey === eventKey && dropPosition === 0) {
                this.setState({
                    _dragOverNodeKey: '',
                    _dropPosition: null
                })
                return
            }

            // Ref: https://github.com/react-component/tree/issues/132
            // Add timeout to let onDragLevel fire before onDragEnter,
            // so that we can clean drag props for onDragLeave node.
            // Macro task for this:
            // https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script
            setTimeout(() => {
                // Update drag over node
                this.setState({
                    _dragOverNodeKey: eventKey,
                    _dropPosition: dropPosition
                })

                // Side effect for delay drag
                if (!this.delayedDragEnterLogic) {
                    this.delayedDragEnterLogic = {}
                }
                Object.keys(this.delayedDragEnterLogic).forEach(key => {
                    clearTimeout(this.delayedDragEnterLogic[key])
                })
                this.delayedDragEnterLogic[pos] = setTimeout(() => {
                    const newExpandedKeys = arrAdd(expandedKeys, eventKey)
                    if (!hasProp(this, 'expandedKeys')) {
                        this.setState({
                            _expandedKeys: newExpandedKeys
                        })
                    }
                    this.__emit('dragenter', {event, node, expandedKeys: newExpandedKeys})
                }, 400)
            }, 0)
        },
        onNodeDragOver(event, node) {
            const {eventKey} = node
            const {_dragOverNodeKey, _dropPosition} = this.$data
            // Update drag position
            if (this.dragNode && eventKey === _dragOverNodeKey && node.$refs.selectHandle) {
                const dropPosition = calcDropPosition(event, node)

                if (dropPosition === _dropPosition) return

                this.setState({
                    _dropPosition: dropPosition
                })
            }
            this.__emit('dragover', {event, node})
        },
        onNodeDragLeave(event, node) {
            this.setState({
                _dragOverNodeKey: ''
            })
            this.__emit('dragleave', {event, node})
        },
        onNodeDragEnd(event, node) {
            this.setState({
                _dragOverNodeKey: ''
            })
            this.__emit('dragend', {event, node})
            this.dragNode = null
        },
        onNodeDrop(event, node) {
            const {_dragNodesKeys = [], _dropPosition} = this.$data

            const {eventKey, pos} = node

            this.setState({
                _dragOverNodeKey: ''
            })

            if (_dragNodesKeys.indexOf(eventKey) !== -1) {
                warning(false, "Can not drop to dragNode(include it's children node)")
                return
            }

            const posArr = posToArr(pos)

            const dropResult = {
                event,
                node,
                dragNode: this.dragNode,
                dragNodesKeys: _dragNodesKeys.slice(),
                dropPosition: _dropPosition + Number(posArr[posArr.length - 1]),
                dropToGap: false
            }

            if (_dropPosition !== 0) {
                dropResult.dropToGap = true
            }
            this.__emit('drop', dropResult)
            this.dragNode = null
        },

        onNodeClick(e, treeNode) {
            this.__emit('click', e, treeNode)
        },

        onNodeDoubleClick(e, treeNode) {
            this.__emit('dblclick', e, treeNode)
        },

        onNodeSelect(e, treeNode) {
            let {_selectedKeys: selectedKeys} = this.$data
            const {_keyEntities: keyEntities} = this.$data
            const {multiple} = this.$props
            const {selected, eventKey} = getOptionProps(treeNode)
            const targetSelected = !selected
            // Update selected keys
            if (!targetSelected) {
                selectedKeys = arrDel(selectedKeys, eventKey)
            } else if (!multiple) {
                selectedKeys = [eventKey]
            } else {
                selectedKeys = arrAdd(selectedKeys, eventKey)
            }

            // [Legacy] Not found related usage in doc or upper libs
            const selectedNodes = selectedKeys
                .map(key => {
                    const entity = keyEntities.get(key)
                    if (!entity) return null

                    return entity.node
                })
                .filter(node => node)

            this.setUncontrolledState({_selectedKeys: selectedKeys})

            const eventObj = {
                event: 'select',
                selected: targetSelected,
                node: treeNode,
                selectedNodes,
                nativeEvent: e
            }
            this.__emit('update:selectedKeys', selectedKeys)
            this.__emit('select', selectedKeys, eventObj)
        },

        getCheckedKeys() {
            // _checkedKeys 是选中的节点
            return this.$data._checkedKeys
        },
        clearExpandedKeys() {
            this.$data._expandedKeys = []
        },
        getHalfCheckedKeys() {
            // _halfCheckedKeys 是半选中的父节点
            return this.$data._halfCheckedKeys
        },

        /**
         * 复选框选中事件
         * @param {*} e
         * @param {*} treeNode
         * @param {*} checked
         */
        onNodeCheck(e, treeNode, checked) {
            const {
                _keyEntities: keyEntities,
                _checkedKeys: oriCheckedKeys,
                _halfCheckedKeys: oriHalfCheckedKeys
            } = this.$data
            const {checkStrictly} = this.$props
            const {eventKey} = getOptionProps(treeNode)

            // Prepare trigger arguments
            let checkedObj
            const eventObj = {
                event: 'check',
                node: treeNode,
                checked,
                nativeEvent: e
            }

            if (checkStrictly) {
                const checkedKeys = checked
                    ? arrAdd(oriCheckedKeys, eventKey)
                    : arrDel(oriCheckedKeys, eventKey)
                const halfCheckedKeys = arrDel(oriHalfCheckedKeys, eventKey)
                checkedObj = {checked: checkedKeys, halfChecked: halfCheckedKeys}

                eventObj.checkedNodes = checkedKeys
                    .map(key => keyEntities.get(key))
                    .filter(entity => entity)
                    .map(entity => entity.node)

                this.setUncontrolledState({_checkedKeys: checkedKeys})
            } else {
                const {checkedKeys, halfCheckedKeys} = conductCheck([eventKey], checked, keyEntities, {
                    checkedKeys: oriCheckedKeys,
                    halfCheckedKeys: oriHalfCheckedKeys
                })

                checkedObj = checkedKeys

                // [Legacy] This is used for `rc-tree-select`
                eventObj.checkedNodes = []
                eventObj.checkedNodesPositions = []
                eventObj.halfCheckedKeys = halfCheckedKeys

                checkedKeys.forEach(key => {
                    const entity = keyEntities.get(key)
                    if (!entity) return

                    const {node, pos} = entity

                    eventObj.checkedNodes.push(node)
                    eventObj.checkedNodesPositions.push({node, pos})
                })

                this.setUncontrolledState({
                    _checkedKeys: checkedKeys,
                    _halfCheckedKeys: halfCheckedKeys
                })
            }
            this.__emit('check', checkedObj, eventObj)
        },
        onNodeLoad(treeNode) {
            return new Promise(resolve => {
                // We need to get the latest state of loading/loaded keys
                this.setState(({_loadedKeys: loadedKeys = [], _loadingKeys: loadingKeys = []}) => {
                    const {loadData} = this.$props
                    const {eventKey} = getOptionProps(treeNode)

                    if (
                        !loadData ||
                        loadedKeys.indexOf(eventKey) !== -1 ||
                        loadingKeys.indexOf(eventKey) !== -1
                    ) {
                        return {}
                    }

                    // Process load data
                    const promise = loadData(treeNode)
                    promise.then(() => {
                        const {_loadedKeys: currentLoadedKeys, _loadingKeys: currentLoadingKeys} = this.$data
                        const newLoadedKeys = arrAdd(currentLoadedKeys, eventKey)
                        const newLoadingKeys = arrDel(currentLoadingKeys, eventKey)

                        // onLoad should trigger before internal setState to avoid `loadData` trigger twice.
                        // https://github.com/ant-design/ant-design/issues/12464
                        this.__emit('load', newLoadedKeys, {
                            event: 'load',
                            node: treeNode
                        })
                        this.setUncontrolledState({
                            _loadedKeys: newLoadedKeys
                        })
                        this.setState({
                            _loadingKeys: newLoadingKeys
                        })
                        resolve()
                    })

                    return {
                        _loadingKeys: arrAdd(loadingKeys, eventKey)
                    }
                })
            })
        },

        onNodeExpand(e, treeNode) {
            let {_expandedKeys: expandedKeys} = this.$data
            const {loadData} = this.$props
            const {eventKey, expanded} = getOptionProps(treeNode)

            // Update selected keys
            const index = expandedKeys.indexOf(eventKey)
            const targetExpanded = !expanded

            warning(
                (expanded && index !== -1) || (!expanded && index === -1),
                'Expand state not sync with index check'
            )

            if (targetExpanded) {
                expandedKeys = arrAdd(expandedKeys, eventKey)
            } else {
                expandedKeys = arrDel(expandedKeys, eventKey)
            }

            this.setUncontrolledState({_expandedKeys: expandedKeys})
            this.__emit('expand', expandedKeys, {
                node: treeNode,
                expanded: targetExpanded,
                nativeEvent: e
            })
            this.__emit('update:expandedKeys', expandedKeys)

            // Async Load data
            if (targetExpanded && loadData) {
                const loadPromise = this.onNodeLoad(treeNode)
                return loadPromise
                    ? loadPromise.then(() => {
                        // [Legacy] Refresh logic
                        this.setUncontrolledState({_expandedKeys: expandedKeys})
                    })
                    : null
            }

            return null
        },

        onNodeMouseEnter(event, node) {
            this.__emit('mouseenter', {event, node})
        },

        onNodeMouseLeave(event, node) {
            this.__emit('mouseleave', {event, node})
        },

        onNodeContextMenu(event, node) {
            event.preventDefault()
            this.__emit('rightClick', {event, node})
        },

        /**
         * Only update the value which is not in props
         */
        setUncontrolledState(state) {
            let needSync = false
            const newState = {}
            const props = getOptionProps(this)
            Object.keys(state).forEach(name => {
                if (name.replace('_', '') in props) return
                needSync = true
                newState[name] = state[name]
            })

            if (needSync) {
                this.setState(newState)
            }
        },

        registerTreeNode(key, node) {
            if (node) {
                this.domTreeNodes[key] = node
            } else {
                delete this.domTreeNodes[key]
            }
        },

        isKeyChecked(key) {
            const {_checkedKeys: checkedKeys = []} = this.$data
            return checkedKeys.indexOf(key) !== -1
        },

        /**
         * [Legacy] Original logic use `key` as tracking clue.
         * We have to use `cloneElement` to pass `key`.
         */
        renderTreeNode(child, index, level = 0) {
            const {
                _keyEntities: keyEntities,
                _expandedKeys: expandedKeys = [],
                _selectedKeys: selectedKeys = [],
                _halfCheckedKeys: halfCheckedKeys = [],
                _loadedKeys: loadedKeys = [],
                _loadingKeys: loadingKeys = [],
                _dragOverNodeKey: dragOverNodeKey,
                _dropPosition: dropPosition
            } = this.$data
            const pos = getPosition(level, index)
            let key = child.key
            if (!key && (key === undefined || key === null)) {
                key = pos
            }
            if (!keyEntities.get(key)) {
                warnOnlyTreeNode()
                return null
            }

            return cloneElement(child, {
                props: {
                    eventKey: key,
                    expanded: expandedKeys.indexOf(key) !== -1,
                    selected: selectedKeys.indexOf(key) !== -1,
                    loaded: loadedKeys.indexOf(key) !== -1,
                    loading: loadingKeys.indexOf(key) !== -1,
                    checked: this.isKeyChecked(key),
                    halfChecked: halfCheckedKeys.indexOf(key) !== -1,
                    pos,

                    // [Legacy] Drag props
                    dragOver: dragOverNodeKey === key && dropPosition === 0,
                    dragOverGapTop: dragOverNodeKey === key && dropPosition === -1,
                    dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1
                },
                key
            })
        }
    },

    render() {
        const {_treeNode: treeNode} = this.$data
        const {prefixCls, focusable, showLine, tabIndex = 0} = this.$props

        return (
            <ul
                class={classNames(prefixCls, {
                    [`${prefixCls}-show-line`]: showLine
                })}
                role="tree"
                unselectable="on"
                tabIndex={focusable ? tabIndex : null}
            >
                {mapChildren(treeNode, (node, index) => this.renderTreeNode(node, index))}
            </ul>
        )
    }
}

export {Tree}

export default proxyComponent(Tree)
